# __HEADER__

"""XXX"""

# XXX: Future:
# - Paged results.
# - Extensible matching (RFC2254):
#   - :dn to include distinguished name attributes
#   - MatchingRuleId
# - Timeouts
# - Excluding some attributes for performance?
# - Sort based on DN.

import ldap.dn
from ldap.filter import escape_filter_chars
import oldap.cache
import oldap.exceptions
from oldap.dn import RDN, DN

MAX_REPR_ITEMS = 20

class Filter(object):

    _base = ''
    _scope = ldap.SCOPE_SUBTREE
    _filter = None
    _model = None
    _sorting = None
    _reversed = False
    _implicit_objectClass = None

    def __init__(self, *args, **kwargs):
        filters = args_to_filters(args, kwargs)
        if len(filters) == 1:
            self._filter = filters[0]
        elif len(filters) == 0:
            self._filter = None
        else:
            self._filter = FilterAnd(filters)

    def _bind(self, model):
        self._model = model
        self._base = model.base_dn
        self._implicit_objectClass = model.objectClass

    def _clone(self, cls=None):
        if cls is None:
            result = self.__class__()
        else:
            result = cls()
        result._base = self._base
        result._scope = self._scope
        if self._filter is not None:
            result._filter = self._filter._clone()
        result._model = self._model
        result._sorting = self._sorting
        result._reversed = self._reversed
        result._implicit_objectClass = self._implicit_objectClass
        return result

    def evaluatable_filter(self):
        if isinstance(self, EvalFilter):
            return self.__class__
        else:
            return EvalFilter

    def _clone_evaluatable(self):
        if self._model is None:
            return self._clone()
        else:
            return self._clone(self.evaluatable_filter())

    def filter_string(self):
        if self._implicit_objectClass:
            f = self & Filter(objectClass=self._implicit_objectClass)
        else:
            f = self
        if f._filter is None:
            return ''
        return str(f._filter)

    def get(self, *args, **kwargs):
        result = self.filter(*args, **kwargs)
        return self._get(result)

    def get_dn(self, dn):
        result = self.base(dn).scope_base()
        return self._get(result)

    def _get(self, result):
        if len(result) == 1:
            return result[0]
        elif len(result) == 0:
            # XXX: Proper exception?  Doesn't include 'matched' or 'desc'.
            raise ldap.NO_SUCH_OBJECT
        else:
            raise oldap.exceptions.MultipleObjectsReturned

    def get_or_create(self, **kwargs):
        """XXX"""
        defaults = kwargs.pop('defaults', {})
        try:
            return self.get(**kwargs), False
        except ldap.NO_SUCH_OBJECT:
            params = dict([(k, v) for k, v in kwargs.items() if '__' not in k])
            params.update(defaults)
            obj = self._model(**params)
            obj.save()
            return obj, True

    ##########################################################################
    # Things that return a Filter.
    ##########################################################################

    def scope_base(self):
        result = self._clone_evaluatable()
        result._scope = ldap.SCOPE_BASE
        return result

    def scope_onelevel(self):
        result = self._clone_evaluatable()
        result._scope = ldap.SCOPE_ONELEVEL
        return result

    def scope_subtree(self):
        result = self._clone_evaluatable()
        result._scope = ldap.SCOPE_SUBTREE
        return result

    def base(self, base):
        result = self._clone_evaluatable()
        result._base = base
        return result

    def present(self, *args):
        items = [FilterItem(arg, '=', '*') for arg in args]
        return self._extend_and(items)

    def all(self):
        return self._clone_evaluatable()

    def filter(self, *args, **kwargs):
        filters = args_to_filters(args, kwargs)
        return self._extend_and(filters)

    def exclude(self, *args, **kwargs):
        filters = args_to_filters(args, kwargs)
        if len(filters) == 1:
            filt = FilterNot(filters[0])
        elif len(filters) == 0:
            return self._clone_evaluatable()
        else:
            filt = FilterNot(FilterAnd(filters))
        return self._extend_and([filt])

    def _extend_and(self, filters):
        result = self._clone_evaluatable()
        if isinstance(result._filter, FilterAnd):
            if len(filters) == 1 and isinstance(filters[0], FilterAnd):
                result._filter._filters.extend(filters[0]._filters)
            else:
                result._filter._filters.extend(filters)
        else:
            if len(filters) != 0:
                if result._filter is None:
                    if len(filters) == 1:
                        result._filter = filters[0]
                    else:
                        result._filter = FilterAnd(filters)
                elif len(filters) == 1:
                    other = filters[0]
                    if isinstance(other, FilterAnd):
                        # _extend_and only accepts new/cloned filters, no reason to reclone.
                        filts = other._filters
                        filts.insert(0, result._filter)
                        result._filter = FilterAnd(filts)
                    else:
                        result._filter = FilterAnd([result._filter, other])
                else:
                    filters.insert(0, result._filter)
                    result._filter = FilterAnd(filters)
        return result

    def __or__(self, other):
        if not isinstance(other, Filter):
            raise TypeError(other)
        result = self._clone_evaluatable()
        if other._filter is None:
            pass
        elif isinstance(self._filter, FilterOr):
            if isinstance(other._filter, FilterOr):
                result._filter._filters.extend([filt._clone() for filt in other._filter._filters])
            else:
                result._filter._filters.append(other._filter._clone())
        else:
            if result._filter is None:
                result._filter = other._filter._clone()
            elif isinstance(other._filter, FilterOr):
                filt = other._filter._clone()
                filt._filters.insert(0, result._filter)
                result._filter = filt
            else:
                result._filter = FilterOr([result._filter, other._filter._clone()])
        return result

    def __and__(self, other):
        if not isinstance(other, Filter):
            raise TypeError(other)
        if other._filter is None:
            return self
        return self._extend_and([other._filter._clone()])

    def __invert__(self):
        result = self._clone_evaluatable()
        if result._filter is not None:
            result._filter = FilterNot(result._filter)
        return result

    def order_by(self, *args):
        result = self._clone_evaluatable()
        if args:
            result._sorting = args
        else:
            result._sorting = None
        return result

    def reverse(self):
        result = self._clone_evaluatable()
        result._reversed = not result._reversed
        return result

    def set_implicit_objectClass(self, objectClass):
        result = self._clone_evaluatable()
        result._implicit_objectClass = objectClass
        return result

    def none(self):
        result = self._clone_evaluatable()
        result._set_empty()
        return result

##############################################################################

class EvalFilter(Filter):

    _results = None

    def __len__(self):
        self._do_query()
        return len(self._results)

    def __iter__(self):
        self._do_query()
        return iter(self._results)

    def __repr__(self):
        self._do_query()
        objs = list(self._results[:MAX_REPR_ITEMS])
        if len(self._results) > MAX_REPR_ITEMS:
            objs.append('...Too many objects to display...')
        return repr(objs)

    def __getitem__(self, k):
        if self._results is None:
            # In the future, if we support paginated results (or incremental
            # retrieval), this should return a Filter instance with the limits
            # recorded.
            self._do_query()

        return self._results[k]

    def iterator(self):
        self._do_query()
        return iter(self._results)

    def _set_empty(self):
        self._results = []

    def _do_query(self):
        if self._results is not None:
            return
        if not self._base:
            raise AssertionError('Cannot execute query without a base.')
        if self._model is None:
            raise AssertionError('Cannot execute query without a model.')
        filter_string = self.filter_string()
        if not filter_string:
            self._results = []
            return
        conn = oldap.cache.get_connection()
        search_result = conn.search_s(self._base, self._scope, filter_string)
        result = []
        for dn, attrs in search_result:
            m = self._model()
            for attr_name, value in attrs.items():
                if attr_name != 'objectClass':
                    attr = m._schema.attributes[attr_name]
                    if attr.single_value:
                        if len(value) > 1:
                            raise AssertionError('SINGLE-VALUE attribute returned multiple values.')
                        setattr(m, attr_name, value[0])
                    else:
                        setattr(m, attr_name, value)
            m._dn = dn
            d = DN(dn)
            m.base_dn = d.base
            m._rdn = d.rdn
            m._snapshot_original()
            result.append(m)
        self._results = result
        if self._sorting:
            self._sort()
        if self._reversed:
            self._results.reverse()

    def _sort(self):
        def _cmp(x, y):
            attrs_x = []
            attrs_y = []
            for attribute in self._sorting:
                if attribute.startswith('-'):
                    attribute = attribute[1:]
                    a,b = y,x
                else:
                    a,b = x,y

                a = getattr(a, attribute, None)
                b = getattr(b, attribute, None)
                if isinstance(a, list):
                    a.sort()
                if isinstance(b, list):
                    b.sort()
                attrs_x.append(a)
                attrs_y.append(b)
            return cmp(attrs_x, attrs_y)
        self._results.sort(_cmp)


##############################################################################

class FilterBase(object):

    """XXX"""

    def _clone(self):
        raise NotImplementedError

class FilterItem(FilterBase):

    def __init__(self, attr, match_type, value):
        self._attr = attr
        self._match_type = match_type
        self._value = value

    def __str__(self):
        return '(%s%s%s)' % (self._attr, self._match_type, self._value)

    def _clone(self):
        return self

class FilterAnd(FilterBase):

    def __init__(self, filters):
        self._filters = filters

    def __str__(self):
        filts = map(str, self._filters)
        return '(&%s)' % (''.join(filts),)

    def _clone(self):
        return self.__class__([filt._clone() for filt in self._filters])

class FilterOr(FilterBase):

    def __init__(self, filters):
        self._filters = filters

    def __str__(self):
        filts = map(str, self._filters)
        return '(|%s)' % (''.join(filts),)

    def _clone(self):
        return self.__class__([filt._clone() for filt in self._filters])

class FilterNot(FilterBase):

    def __init__(self, filter):
        self._filter = filter

    def __str__(self):
        return '(!%s)' % (self._filter,)

    def _clone(self):
        return self.__class__(self._filter._clone())


##############################################################################

def _kwarg_approximate(value):
    return '~=', escape_filter_chars(value)

def _kwarg_startswith(value):
    return '=', escape_filter_chars(value)+'*'

def _kwarg_endswith(value):
    return '=', '*'+escape_filter_chars(value)

def _kwarg_contains(value):
    return '=', '*'+escape_filter_chars(value)+'*'

def _kwarg_wild(value):
    return '=', value

def _kwarg_gte(value):
    return '>=', escape_filter_chars(value)

def _kwarg_lte(value):
    return '<=', escape_filter_chars(value)


def args_to_filters(args, kwargs):
    for arg in args:
        if not isinstance(arg, Filter):
            raise TypeError('Arguments must by filters, not %s' % (type(arg),))
    result = [arg._filter for arg in args if arg._filter is not None]
    for kwarg, value in kwargs.items():
        if isinstance(value, tuple):
            value = list(value)
        elif not isinstance(value, list):
            value = [value]
        parts = kwarg.split('__')
        if len(parts) == 1:
            for v in value:
                result.append(FilterItem(parts[0], '=', escape_filter_chars(v)))
        elif len(parts) == 2:
            kwarg_func = globals()['_kwarg_%s' % parts[1]]
            for v in value:
                result.append(FilterItem(parts[0], *kwarg_func(v)))
        else:
            raise TypeError('Too many underscores in attribute name.')
    return result
